package fast.calc;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import fast.data.RandomSuffix;
import fast.data.Stat;
import fast.data.WeaponType;

public abstract class Fast {
	
	protected Stats stats;
	
	protected Map<String, Double> ep = new HashMap<String, Double>();
	
	protected Map<String, Double> aps = new HashMap<String, Double>();
	
	protected double dps1 = 0.;
	protected Map<String, Double> dpsBreakdown = null;
	
	protected static final double FIGHT_LENGTH = 360.;
	protected static final double PRECISION = Math.pow(10, -8);
	
	public void calc_ep(){
		// calc ep
		stats.setStat(Stat.ap, stats.getStat(Stat.ap)+1);
		double dps_ap = get_dps();
		stats.setStat(Stat.ap, stats.getStat(Stat.ap)-1);
		double norm = dps_ap - dps1;
		ep.put("ap", (dps_ap - dps1)/norm);
		
		stats.setStat(Stat.agi, stats.getStat(Stat.agi)+1);
		double dps_agi = get_dps();
		stats.setStat(Stat.agi, stats.getStat(Stat.agi)-1);
		ep.put("agi", (dps_agi - dps1)/norm);
		
		stats.setStat(Stat.str, stats.getStat(Stat.str)+1);
		double dps_str = get_dps();
		stats.setStat(Stat.str, stats.getStat(Stat.str)-1);
		ep.put("str", (dps_str - dps1)/norm);
				
		stats.setStat(Stat.cri, stats.getStat(Stat.cri)+1);
		double dps_crit = get_dps();
		stats.setStat(Stat.cri, stats.getStat(Stat.cri)-1);
		ep.put("crit", (dps_crit - dps1)/norm);
		
		stats.setStat(Stat.mst, stats.getStat(Stat.mst)+1);
		double dps_mastery = get_dps();
		stats.setStat(Stat.mst, stats.getStat(Stat.mst)-1);
		ep.put("mastery", (dps_mastery - dps1)/norm);
			
		stats.setStat(Stat.hst, stats.getStat(Stat.hst)+1);
		double dps_haste = get_dps();
		stats.setStat(Stat.hst, stats.getStat(Stat.hst)-1);
		ep.put("haste", (dps_haste - dps1)/norm);
		
		stats.calcEP = "style_hit";
		double dps_style_hit = get_dps();
		stats.calcEP = "";
		ep.put("style_hit", Math.abs(dps_style_hit - dps1)/norm);
		
		stats.calcEP = "white_hit";
		double dps_white_hit = get_dps();
		stats.calcEP = "";
		ep.put("white_hit", Math.abs(dps_white_hit - dps1)/norm);
		
		stats.calcEP = "exp_mh";
		double dps_exp_mh = get_dps();
		stats.calcEP = "exp_oh";
		double dps_exp_oh = get_dps();
		stats.calcEP = "";
		ep.put("mh_exp", Math.abs(dps_exp_mh - dps1)/norm);
		ep.put("oh_exp", Math.abs(dps_exp_oh - dps1)/norm);
		ep.put("exp", Math.abs(dps_exp_mh - dps1)/norm + Math.abs(dps_exp_oh - dps1)/norm);
		
		stats.getMh().setDps(stats.getMh().getDps()+1);
		double dps_mh_dps = get_dps();
		stats.getMh().setDps(stats.getMh().getDps()-1);
		ep.put("mh_dps", Math.abs(dps_mh_dps - dps1)/norm);
		
		stats.getOh().setDps(stats.getOh().getDps()+1);
		double dps_oh_dps = get_dps();
		stats.getOh().setDps(stats.getOh().getDps()-1);
		ep.put("oh_dps", Math.abs(dps_oh_dps - dps1)/norm);
		
		double ori_mh_speed = stats.getMh().getSpeed();
		for(double speed = 1.4; speed < 2.8; speed += 0.1){
			speed = Math.round(speed*10)/10.0d;
			stats.getMh().setSpeed(speed);
			double dps_mh_speed = get_dps();
			ep.put("mh_speed_"+speed, (dps_mh_speed - dps1)/norm);
		}
		stats.getMh().setSpeed(ori_mh_speed);
		
		double ori_oh_speed = stats.getOh().getSpeed();
		for(double speed = 1.4; speed < 2.8; speed += 0.1){
			speed = Math.round(speed*10)/10.0d;
			stats.getOh().setSpeed(speed);
			double dps_oh_speed = get_dps();
			ep.put("oh_speed_"+speed, (dps_oh_speed - dps1)/norm);
		}
		stats.getOh().setSpeed(ori_oh_speed);
		
		WeaponType ori_mh_type = stats.getMh().getType();
		for(WeaponType wtype : WeaponType.values()){
			stats.getMh().setType(wtype);
			double dps_mh_type = get_dps();
			ep.put("mh_type_"+wtype.name(), (dps_mh_type - dps1)/norm);
		}
		stats.getMh().setType(ori_mh_type);
		
		WeaponType ori_oh_type = stats.getOh().getType();
		for(WeaponType wtype : WeaponType.values()){
			stats.getOh().setType(wtype);
			double dps_oh_type = get_dps();
			ep.put("oh_type_"+wtype.name(), (dps_oh_type - dps1)/norm);
		}
		stats.getOh().setType(ori_oh_type);
		
		// gearBuffs
		List<GearBuffData> gbd = stats.getGearBuffs().getAll();
		List<String> cache = new ArrayList<String>((stats.getGearBuffs().getGearBuffs()));
		HashMap<String,Integer> cache_ilvl = new HashMap<String,Integer>(stats.getGearBuffs().getGearBuffItemLevel());
		stats.getGearBuffs().getGearBuffs().clear();
		stats.getGearBuffs().getGearBuffItemLevel().clear();
		double dps_nothing_gb = get_dps();
		stats.setStat(Stat.ap, stats.getStat(Stat.ap)+1);
		double dps_nothing_norm = get_dps();
		stats.setStat(Stat.ap, stats.getStat(Stat.ap)-1);
		for (GearBuffData gb : gbd){
			if(gb.scaling){
				boolean equiped = stats.getGearBuffs().has(gb.name);
				int equipedItemLevel = -1;
				if(equiped){
					equipedItemLevel = stats.getGearBuffs().getGearBuffItemLevel().get(gb.name);
					stats.getGearBuffs().getGearBuffs().remove(gb.name);
				}
				double dps_no_gb = get_dps();
				stats.setStat(Stat.ap, stats.getStat(Stat.ap)+1);
				double dps_norm = get_dps();
				stats.setStat(Stat.ap, stats.getStat(Stat.ap)-1);
				int lvl_steps = 4;	int upgrade_steps = 2;
				if(gb.blue){ lvl_steps = 8; upgrade_steps = 1;}
				for(int ilvl = gb.base_lvl; ilvl <= gb.base_lvl+lvl_steps*upgrade_steps; ilvl += lvl_steps){
					stats.getGearBuffs().getGearBuffs().add(gb.name);
					stats.getGearBuffs().getGearBuffItemLevel().put(gb.name, ilvl);
					double dps_gb = get_dps();
					stats.calcEP = gb.name+"_"+ilvl;
					ep.put(gb.name+"_"+ilvl, Math.abs(dps_gb - dps_no_gb)/(dps_norm - dps_no_gb));
					stats.getGearBuffs().getGearBuffs().remove(gb.name);
				}
				if(equipedItemLevel != -1){
					stats.getGearBuffs().getGearBuffs().add(gb.name);
					stats.getGearBuffs().getGearBuffItemLevel().put(gb.name, equipedItemLevel);
				}else{
					stats.getGearBuffs().getGearBuffs().remove(gb.name);
					stats.getGearBuffs().getGearBuffItemLevel().remove(gb.name);
				}
			}
			else{
				boolean equiped = stats.getGearBuffs().has(gb.name);
				if(equiped)
					stats.getGearBuffs().getGearBuffs().remove(gb.name);
				else
					stats.getGearBuffs().getGearBuffs().add(gb.name);
				stats.calcEP = gb.name;
				double dps_gb = get_dps();
				ep.put(gb.name, Math.abs(dps_gb - dps_nothing_gb)/(dps_nothing_norm - dps_nothing_gb));
				if(equiped)
					stats.getGearBuffs().getGearBuffs().add(gb.name);
				else
					stats.getGearBuffs().getGearBuffs().remove(gb.name);
			}
		}
		stats.getGearBuffs().setGearBuffs(cache);
		stats.getGearBuffs().setGearBuffItemLevel(cache_ilvl);
		
		// sets
		List<String> setBuffs = stats.getGearBuffs().getAllSets();
		for (String setBuff : setBuffs){
			boolean equiped = stats.getGearBuffs().hasSet(setBuff);
			if(equiped)
				stats.getGearBuffs().getSetItems().remove(setBuff);
			else
				stats.getGearBuffs().getSetItems().add(setBuff);
			stats.calcEP = setBuff;
			double dps_sb = get_dps();
			ep.put(setBuff, Math.abs(dps_sb - dps1)/norm);
			if(equiped)
				stats.getGearBuffs().getSetItems().add(setBuff);
			else
				stats.getGearBuffs().getSetItems().remove(setBuff);
		}
		// Settings
		List<String> settings = stats.getGearBuffs().getAllSettings();
		for (String setting : settings){
			boolean equiped = stats.getGearBuffs().has(setting);
			if(equiped)
				stats.getGearBuffs().getGearBuffs().remove(setting);
			else
				stats.getGearBuffs().getGearBuffs().add(setting);
			double dps_gb = get_dps();
			stats.calcEP = setting;
			ep.put(setting, Math.abs(dps_gb - dps1)/norm);
			if(equiped)
				stats.getGearBuffs().getGearBuffs().add(setting);
			else
				stats.getGearBuffs().getGearBuffs().remove(setting);
		}
		
		// meta gems
		boolean equiped = stats.getGearBuffs().has("metagem_critdmg");
		if(equiped)
			stats.getGearBuffs().getGearBuffs().remove("metagem_critdmg");
		else
			stats.getGearBuffs().getGearBuffs().add("metagem_critdmg");
		double dps_gb = get_dps();
		stats.calcEP = "metagem_critdmg";
		ep.put("metagem_critdmg", Math.abs(dps_gb - dps1)/norm);
		if(equiped)
			stats.getGearBuffs().getGearBuffs().add("metagem_critdmg");
		else
			stats.getGearBuffs().getGearBuffs().remove("metagem_critdmg");
		
		stats.calcEP = "";
	}
	
	protected abstract double get_dps();
	
	protected abstract double get_total_speed();
	
	protected double get_hasted_speed(){
		double speed = 1 * (1 + 0.01 * stats.getBuffed(Stat.hst) / Stats.HASTE_CONVERSION_RATING);
		if(stats.getGearBuffs().has("berserking"))
			speed *= 1 + .2 * 10. / 180;
		
		if(stats.getBuffs().hasBuff("casterhaste_buff"))
			speed *= 1.05;
		
		speed *= (1 + 0.3 * 40 / FIGHT_LENGTH);
		return speed;
	}
	
	protected double get_hasted_speed(double haste){
		double speed = 1 * (1 + 0.01 * haste / Stats.HASTE_CONVERSION_RATING);
		if(stats.getGearBuffs().has("berserking"))
			speed *= 1 + .2 * 10. / 180;
		
		if(stats.getBuffs().hasBuff("casterhaste_buff"))
			speed *= 1.05;
		
		speed *= (1 + 0.3 * 40 / FIGHT_LENGTH);
		return speed;
	}

	public double getDps() {
		return dps1;
	}

	public abstract Map<String, Double> getEP();

	public Stats getStats() {
		return stats;
	}

	public HashMap<String,Double> getDpsBreakdown() {
		return (HashMap<String, Double>) dpsBreakdown;
	}
	
	private double get_lambda(GearBuffData gbd, double haste){
		double hasted_speed = 1.;
		if(gbd.haste_scaling) 
			hasted_speed = get_hasted_speed(haste);
		return hasted_speed * gbd.rppm / 60 * gbd.duration;
	}
	
	private double get_scaling_value(GearBuffData gbd){
		if(stats.getGearBuffs().getGearBuffItemLevel().containsKey(gbd.name)){
			int itemlevel = stats.getGearBuffs().getGearBuffItemLevel().get(gbd.name);
			int scaling = RandomSuffix.RandomPropData[itemlevel-277][0][0]; // TODO change it
			return scaling;
		}
		return 0.0;
	}
	
	protected final void procs(double total_mh_hits, double total_oh_hits, double total_other_hits){
		stats.getProcs().subtract(stats.getProcs()); // reset to zero
		for(String gearBuff : stats.getGearBuffs().getGearBuffs()){
			GearBuffData gbd = stats.getGearBuffs().getGearBuffData(gearBuff);
			if(gbd == null){
				continue;
			}
			if(gbd.is_buff()){
				double procs_per_second = get_procs_per_second(gbd, total_mh_hits, total_oh_hits, total_other_hits);
				double uptime = 1.0;
				if(gbd.hasICD()){
					if(gbd.isRealPPM()){
						//double time_to_proc = (1 - (1 - (2 - (5 / (Math.exp(3./2)))))*Math.exp(procs_per_second * gbd.icd)) / procs_per_second + gbd.icd;
						double hasted_speed = 1.;
						if(gbd.haste_scaling)
							hasted_speed = get_hasted_speed();
						double time_to_proc = 60/(hasted_speed * gbd.rppm) + gbd.icd - 10;
						//System.out.println(gbd.name + "time_to_proc " + time_to_proc);
						uptime = 1.1307 * gbd.duration / time_to_proc;
					}else{
						//System.out.println(gbd.name + " procs_every " + 1 / procs_per_second);
						uptime *= gbd.duration / (gbd.icd + 1 / procs_per_second);
					}
				}
				if(!gbd.hasICD() && (gbd.isPPM() || gbd.isRealPPM())){
					if(gbd.isRealPPM()){
						if(gbd.hasStacks()){
							if(gbd.proc.getStat(Stat.hst) > 0){
								double base_value = this.stats.getStat(Stat.hst);
								double stack_value = Math.round(get_scaling_value(gbd) * gbd.proc.getStat(Stat.hst));
								double upt = 0.0;
								for(int i = 1; i <= gbd.stacks; i++){
									double lambda = get_lambda(gbd, base_value + stack_value * i);
									double base = (1 - Math.exp(-lambda));
									upt += Math.pow(base, i);
								}
								uptime = upt * 1.1307;
							}else{
								double lambda = get_lambda(gbd, this.stats.getStat(Stat.hst));
								double e_lambda = Math.exp(lambda);
								double e_minus_lambda = Math.exp(-lambda);
								uptime = 1.1307 * (e_lambda-1)*(1 - Math.pow((1-e_minus_lambda),gbd.stacks));
							}
						}else{
							double lambda = get_lambda(gbd, stats.getBuffed(Stat.hst));
							uptime = 1.1307 * (1 - Math.exp(-lambda));
						}
					}else{
						if(gbd.hasStacks()){
							double q = 1 - procs_per_second;
			                double Q = Math.pow(q , gbd.duration);
			                double P = 1 - Q;
			                uptime = P * (1 - Math.pow(P, gbd.stacks)) / Q;
						} else{
							uptime *= 1 - Math.pow(1 - procs_per_second, gbd.duration);
						}
					}
				}
				else if(gbd.hasStacks()){
					uptime *= gbd.stacks;
				}
				//System.out.println(gbd.name + " uptime " + uptime);
				if(gbd.scaling){
					if(stats.getGearBuffs().getGearBuffItemLevel().containsKey(gbd.name)){
						int itemlevel = stats.getGearBuffs().getGearBuffItemLevel().get(gbd.name);
						int scaling = RandomSuffix.RandomPropData[itemlevel-277][0][0]; // TODO change it
						stats.getProcs().add(gbd.proc.scale(uptime, scaling));
					}
				}else{
					// a bit silly and long could be better implemented
					if(gbd.name.startsWith("rune_of_reorigination")){
						Procs rune = new Procs();
						HashMap<Stat,Double> statsBuffed = new HashMap<Stat,Double>();
						double hst = stats.getStat(Stat.hst);
						double mst = stats.getStat(Stat.mst); // (stats.getBuffed(Stat.mst)-8.) * Stats.MASTERY_CONVERSION_RATING;
						if(stats.getBuffs().hasBuff("mastery_buff"))
							mst += 3000;
						double cri = stats.getStat(Stat.cri);
						statsBuffed.put(Stat.hst, hst);
						statsBuffed.put(Stat.mst, mst);
						statsBuffed.put(Stat.cri, cri);
//						System.out.println("stats buffed");
//						for(Stat tt : statsBuffed.keySet()){
//							System.out.println(tt + " " + statsBuffed.get(tt));
//						}
						Stat highest = Stat.mst;
						if(hst > statsBuffed.get(highest)) 
							highest = Stat.hst;
						if(cri > statsBuffed.get(highest))
							highest = Stat.cri;
						if(highest == Stat.hst){
							hst = (mst+cri)*2;
							mst = -mst;
							cri = -cri;
						}else if(highest == Stat.mst){
							mst = (hst+cri)*2;
							hst = -hst;
							cri = -cri;
						}else if(highest == Stat.cri){
							cri = (hst+mst)*2;
							hst = -hst;
							mst = -mst;
						}
						rune.setStat(Stat.hst, hst);
						rune.setStat(Stat.mst, mst);
						rune.setStat(Stat.cri, cri);
						//System.out.println(rune.toString());
						gbd.proc = rune;
					}
					stats.getProcs().add(gbd.proc.scale(uptime));
				}
			}
			if(gbd.is_direct_dmg()){
				if(gbd.name.equals("capacitive_primal_diamond")){
					// modify rppm by spec
					gbd.rppm = stats.getGearBuffs().getCapacitivePrimalDiamondRPPM(stats.getSpec());
				}
				double procs_per_second = get_procs_per_second(gbd, total_mh_hits, total_oh_hits, total_other_hits);
				if(gbd.name.endsWith("elemental_force")){
					aps.put(gearBuff, procs_per_second * stats.getSpellHitChance());
				}
				else {
					double time_to_proc = (gbd.icd + 1 / procs_per_second);
					if(gbd.isRealPPM())
						//time_to_proc = (2 - (5 / (Math.exp(3./2)))) / procs_per_second;
						time_to_proc = 1 / procs_per_second;
					//System.out.println(gbd.name + " ttp " + time_to_proc);
					double aps3 = 1.0 / time_to_proc;
					if(gbd.hasStacks()){
						// we are only having the case that something makes dmg when at
						// #stacks, so this is correct, if we get another mechanic we need
						// to change this
						aps3 /= gbd.stacks;
					}
					aps.put(gearBuff, aps3);
				}
			}
		}
	}
	
	private final double get_procs_per_second(GearBuffData gbd, double mh_hits_per_sec, double oh_hits_per_sec, double other_hits_per_sec){
		if(gbd.isRealPPM())
			return proc_rate(gbd, 1.8);
		double valid_hits_per_sec = mh_hits_per_sec + oh_hits_per_sec + other_hits_per_sec;
		double proc_rate = proc_rate(gbd, 1.8);
		if(gbd.mh_only()) {
			valid_hits_per_sec = mh_hits_per_sec;
			proc_rate = proc_rate(gbd, stats.getMh().getSpeed());
		}else if(gbd.oh_only()) {
			valid_hits_per_sec = oh_hits_per_sec;
			proc_rate = proc_rate(gbd, stats.getOh().getSpeed());
		}
		if(gbd.onlyCrit()) {
			valid_hits_per_sec *= stats.getCritPercentStatic();
		}
		return valid_hits_per_sec * proc_rate;
	}
	
	private final double proc_rate(GearBuffData gbd, double speed){
        if(gbd.isPPM())
        	return gbd.ppm * speed / 60.;
        else if (gbd.isRealPPM()){
        	double hasted_speed = 1.;
        	if(gbd.haste_scaling) 
        		hasted_speed = get_hasted_speed();
            return hasted_speed * gbd.rppm / 60;
        }
        else
            return gbd.proc_chance;
	}
}
